package cmd

import (
	"bufio"
	"fmt"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"regexp"
	"strings"

	config "code.alibaba-inc.com/agit/quarantine-helper/config"
	"code.alibaba-inc.com/agit/quarantine-helper/env"
	"code.alibaba-inc.com/agit/quarantine-helper/utils"
	"github.com/pkg/errors"
	"github.com/spf13/cobra"
)

const (
	CommitsRelativePath = "info/commits"
)

func visit(loosFiles, packFiles *[]string) filepath.WalkFunc {
	return func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if info.IsDir() || filepath.Ext(path) == ".keep" || filepath.Ext(path) == ".pack" || filepath.Ext(path) == ".DS_Store" {
			return nil
		} else if filepath.Ext(path) == ".idx" {
			*packFiles = append(*packFiles, path)
		} else if isValidLooseFile(path) {
			*loosFiles = append(*loosFiles, path)
		} else {
			//other files we don't care here, such as info/large-blobs
		}
		return nil
	}
}

func getSha(path string) string {
	paths := strings.Split(path, "/")
	len := len(paths)
	return paths[len-2] + paths[len-1]
}

func isValidLooseFile(path string) bool {
	paths := strings.Split(path, "/")
	length := len(paths)
	if length < 2 {
		return false
	}
	re := regexp.MustCompile(`^[a-f0-9]{2}$`)
	if !re.MatchString(paths[length-2]) {
		return false
	}
	re = regexp.MustCompile(`^[a-f0-9]{38}`)
	if !re.MatchString(paths[length-1]) {
		return false
	}
	return true
}

func scanPackFile(path string, env []string) ([]string, error) {
	ids := []string{}

	cmd := exec.Command("git", "verify-pack", "-v", path)
	if len(env) > 0 {
		cmd.Env = env
	}
	cmd.Stderr = os.Stderr
	/*
	 * We used to cleanup env and only leave "config.GitRepositoryPath"
	 * and GitConfigParameters. This is because we used to execute
	 * git verify-pack in a shell command. This is not suitable for
	 * this new implement: we execute "git verify-pack" directly.
	 */
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, errors.New("git-verify-pack: stdout pipe failed")
	}
	if err = cmd.Start(); err != nil {
		return nil, errors.Errorf("git-verify-pack: fail to start: %s", err)
	}
	scanner := bufio.NewScanner(stdout)
	for scanner.Scan() {
		items := strings.Split(scanner.Text(), " ")
		if len(items) < 2 || items[1] != "commit" {
			continue
		}
		ids = append(ids, items[0])
	}
	if err := cmd.Wait(); err != nil {
		return nil, errors.Errorf("git-verify-pack: wait fail: %s", err)
	}

	return ids, nil
}

func getLooseCommits(looseFiles []string, env []string) ([]string, error) {
	var (
		ids []string
	)

	for _, file := range looseFiles {
		ids = append(ids, getSha(file))
	}
	return utils.FilterObjects(ids, "commit", env)
}

func normalScan() ([]string, error) {
	var (
		looseFiles []string
		packFiles  []string
		ids        []string
		err        error
	)

	err = filepath.Walk(config.GetQuarantinePath(), visit(&looseFiles, &packFiles))
	if err != nil {
		return ids, err
	}

	ids, err = getLooseCommits(looseFiles, env.GetEnvsWithQuarantine())
	if err != nil {
		return nil, err
	}

	for _, file := range packFiles {
		idsFromPack, err := scanPackFile(file, env.GetEnvsWithQuarantine())
		if err != nil {
			return nil, err
		}
		ids = append(ids, idsFromPack...)
	}

	if !config.GetDisableStdout() {
		for _, id := range ids {
			fmt.Println(id)
		}
	}

	return ids, nil
}

func quickScan(fileName string) ([]string, error) {
	quarantine := config.GetQuarantinePath()
	filePath := path.Join(quarantine, fileName)
	if _, err := os.Stat(filePath); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil, fmt.Errorf("fail to read IDs from: %v, file not exist", filePath)
		}
		return nil, fmt.Errorf("fail to check status of file: %v, err: %v", filePath, err)
	}
	file, err := os.Open(filePath)
	if err != nil {
		return nil, fmt.Errorf("fail to open file: %v; err: %v", filePath, err)
	}
	defer file.Close()
	ids := []string{}
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		ids = append(ids, line)
		if !config.GetDisableStdout() {
			fmt.Println(line)
		}
	}
	if scanner.Err() != nil {
		return nil, fmt.Errorf(("fail to scan tree ids from file: %v, err: %v"), filePath, scanner.Err())
	}
	return ids, nil
}

func scan() ([]string, error) {
	var (
		ids []string
		err error
	)

	if config.IsCommitFromWeb() {
		return nil, nil
	}
	if config.NoQuickScan {
		return normalScan()
	}
	ids, err = quickScan(CommitsRelativePath)
	if err != nil {
		if config.QuickScan {
			fmt.Fprintf(os.Stderr, "ERROR: fail to quick scan: %s\n", err)
		}
		ids, err = normalScan()
	}
	return ids, err
}

var scanCmd = &cobra.Command{
	Use:   "scan",
	Short: "Scanning files in Quarantine to find commit objects and extract their commitIDs.",
	Long: `This command scans objects(both loose object and packfile) in Quarantine in the phase of pre-receive hook, 
	extract commit objects, and output their commitIDs one by one.`,
	RunE: func(cmd *cobra.Command, args []string) error {
		_, err := scan()
		return err
	},
}

func init() {
	rootCmd.AddCommand(scanCmd)
	scanCmd.Flags().BoolVarP(&config.QuickScan, "quickScan", "", false, "read commit ids from $GIT_QUARANTINE_PATH/info/commits")
	scanCmd.Flags().BoolVarP(&config.NoQuickScan, "no-quickscan", "", false, "read commit ids from objdir")
}
